﻿using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shell;
using System.Threading;
using System.Threading.Tasks;

namespace Snip
{
    /// <summary>
    /// Interaction logic for ProgressDialog.xaml
    /// </summary>
    public partial class ProgressDialog : Window
    {
        private const int FILESTREAM_BUFFER_SIZE = 4096;
        private const int BUFFER_SIZE = 1024;
        private const int TEMP_BUFFER_SIZE = 20;
        private const int PROGRESS_THRESHOLD = 1024 * 10;

        private BackgroundWorker worker;
        private string sourceFile;
        private SplitOptions options;
        private DateTime now = DateTime.Now;

        private Stream stream;
        private byte[] byteBuffer;
        private int byteLen;
        private int bytePos;
        private byte[] tempBuffer;
        private int tempPos;

        public ProgressDialog(string sourceFile, SplitOptions options)
        {
            InitializeComponent();
            this.sourceFile = sourceFile;
            this.options = options;

            worker = new BackgroundWorker();
            worker.WorkerReportsProgress = true;
            worker.WorkerSupportsCancellation = true;
            worker.DoWork += new DoWorkEventHandler(worker_DoWork);
            worker.ProgressChanged += new ProgressChangedEventHandler(worker_ProgressChanged);
            worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);

            Loaded += new RoutedEventHandler(ProgressDialog_Loaded);
            Closing += new CancelEventHandler(ProgressDialog_Closing);
        }

        private void ProgressDialog_Loaded(object sender, RoutedEventArgs e)
        {
            TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Normal;

            worker.RunWorkerAsync();

            ProgressSizeTextBlock.Text = Util.StrFormatByteSize(0);
            FileInfo fileInfo = new FileInfo(sourceFile);
            FileSizeTextBlock.Text = Util.StrFormatByteSize(fileInfo.Length);
        }

        private void ProgressDialog_Closing(object sender, CancelEventArgs e)
        {
            if (worker.IsBusy)
            {
                worker.CancelAsync();
                e.Cancel = true;
            }
        }

        private void CancelButton_Click(object sender, RoutedEventArgs e)
        {
            CancelButton.IsEnabled = false;
            worker.CancelAsync();
        }


        private int ReadBuffer()
        {
            byteLen = stream.Read(byteBuffer, 0, byteBuffer.Length);
            bytePos = 0;
            return byteLen;
        }

        private bool TryReadTimestamp(DateTime previous, out DateTime timestamp)
        {
            int month, day, hour, minute, second, millisecond;
            if (!ReadNumber('/', 1, 2, out month) || !ReadNumber(' ', 1, 2, out day) || !ReadNumber(':', 2, 2, out hour) || !ReadNumber(':', 2, 2, out minute) || !ReadNumber('.', 2, 2, out second) || !ReadNumber(' ', 3, 3, out millisecond))
            {
                timestamp = default(DateTime);
                return false;
            }

            if ((bytePos == byteLen && ReadBuffer() == 0) || byteBuffer[bytePos] != ' ')
            {
                timestamp = default(DateTime);
                return false;
            }

            tempBuffer[tempPos++] = byteBuffer[bytePos];
            bytePos++;

            timestamp = new DateTime(now.Year, month, day, hour, minute, second, millisecond);
            if (previous != default(DateTime) && previous > timestamp)
            {
                timestamp = timestamp.AddYears(+1);
            }
            else if (timestamp > now)
            {
                timestamp = timestamp.AddYears(-1);
            }
            return true;
        }

        private bool ReadNumber(char delim, int minLen, int maxLen, out int value)
        {
            value = 0;
            if (bytePos == byteLen && ReadBuffer() == 0) return false;

            int n = 0;
            char ch = (char)byteBuffer[bytePos];
            if (ch < '0' || ch > '9') return false;
            do
            {
                value = value * 10 + ch - '0';
                tempBuffer[tempPos++] = (byte)ch;
                bytePos++;
                n++;
                if (n > maxLen) return false;
                if (bytePos == byteLen && ReadBuffer() == 0) break;
                ch = (char)byteBuffer[bytePos];
            } while (ch >= '0' && ch <= '9');

            if (ch != delim) return false;
            tempBuffer[tempPos++] = (byte)ch;
            bytePos++;

            if (n < minLen) return false;
            return true;
        }

        private void WriteLine(Stream tempStream)
        {
            tempStream.Write(tempBuffer, 0, tempPos);
            tempPos = 0;

            if (bytePos == byteLen)
            {
                if (ReadBuffer() == 0) return;
            }

            do
            {
                int i = bytePos;
                do
                {
                    byte b = byteBuffer[i];
                    if (b == '\r' || b == '\n')
                    {
                        tempStream.Write(byteBuffer, bytePos, i - bytePos + 1);
                        bytePos = i + 1;
                        if (b == '\r' && (bytePos < byteLen || ReadBuffer() > 0))
                        {
                            if (byteBuffer[bytePos] == '\n')
                            {
                                tempStream.WriteByte(byteBuffer[bytePos]);
                                bytePos++;
                            }
                        }
                        return;
                    }
                    i++;
                } while (i < byteLen);
                i = byteLen - bytePos;
                tempStream.Write(byteBuffer, bytePos, i);
            } while (ReadBuffer() > 0);
        }

        private void worker_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker worker = (BackgroundWorker)sender;
            using (stream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read, FILESTREAM_BUFFER_SIZE, FileOptions.SequentialScan))
            {
                byteBuffer = new byte[BUFFER_SIZE];
                tempBuffer = new byte[TEMP_BUFFER_SIZE];

                DateTime start = default(DateTime);
                DateTime previous = default(DateTime);

                string tempFile = null;
                FileStream tempStream = null;
                DateTime timestamp;

                long lastStreamPos = 0;
                try
                {
                    if (ReadBuffer() == 0)
                    {
                        return;
                    }
                    if (byteLen >= 3 && byteBuffer[0] == 0xEF && byteBuffer[1] == 0xBB && byteBuffer[2] == 0xBF) bytePos = 3;

                    while (bytePos < byteLen || ReadBuffer() > 0)
                    {
                        if (worker.CancellationPending)
                        {
                            e.Cancel = true;
                            break;
                        }

                        if (!TryReadTimestamp(previous, out timestamp))
                        {
                            throw new ApplicationException("Couldn't parse timestamp! Maybe the file is malformed?");
                        }

                        TimeSpan interval = timestamp - previous;
                        if (interval >= options.Interval && tempStream != null)
                        {
                            tempStream.Flush();
                            tempStream.Close();
                            tempStream = null;
                            RenameTempFile(tempFile, start, previous);
                            tempFile = null;
                        }

                        if (tempStream == null)
                        {
                            tempStream = CreateTempStream(out tempFile);
                            start = timestamp;
                        }

                        WriteLine(tempStream);
                        previous = timestamp;

                        long streamPos = Math.Max(0, stream.Position - BUFFER_SIZE + bytePos);
                        if ((streamPos - lastStreamPos) >= PROGRESS_THRESHOLD)
                        {
                            int percentComplete = (int)((double)streamPos / stream.Length * 100);
                            worker.ReportProgress(percentComplete, streamPos);
                            lastStreamPos = streamPos;
                        }
                    }

                    if (tempStream != null)
                    {
                        if (e.Cancel)
                        {
                            tempStream.Close();
                            tempStream = null;
                            File.Delete(tempFile);
                        }
                        else
                        {
                            tempStream.Flush();
                            tempStream.Close();
                            tempStream = null;
                            RenameTempFile(tempFile, start, previous);
                            tempFile = null;
                        }
                    }
                }
                finally
                {
                    if (tempStream != null)
                    {
                        try
                        {
                            tempStream.Close();
                            tempStream = null;
                            File.Delete(tempFile);
                        }
                        catch { }
                    }
                }
            }

            //if (!e.Cancel)
            //{
            //    if (options.DeleteSourceFileOnComplete)
            //    {
            //        try
            //        {
            //            File.Delete(sourceFile);
            //        }
            //        catch { }
            //    }

            //    if (options.OpenDestFolderOnComplete)
            //    {
            //        string dir = options.DestFolder;
            //        if (string.IsNullOrWhiteSpace(dir))
            //        {
            //            dir = Path.GetDirectoryName(sourceFile);
            //        }

            //        Process process = new Process();
            //        process.StartInfo.FileName = dir;
            //        process.Start();
            //    }
            //}
        }

        internal void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            ProgressBar.Value = e.ProgressPercentage;
            TaskbarItemInfo.ProgressValue = e.ProgressPercentage / 100d;

            if (e.UserState is long)
            {
                long bytes = (long)e.UserState;
                ProgressSizeTextBlock.Text = Util.StrFormatByteSize(bytes);
            }
        }

        internal void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            TaskbarItemInfo.ProgressState = TaskbarItemProgressState.None;

            if (e.Error != null)
            {
                MessageBox.Show(this, e.Error.Message, "Oops", MessageBoxButton.OK, MessageBoxImage.Error);
                DialogResult = false;
            }
            else if (e.Cancelled)
            {
                MessageBox.Show(this, "You cancelled this operation.", "Cancelled");
                DialogResult = false;
            }
            else
            {
                if (options.DeleteSourceFileOnComplete)
                {
                    try
                    {
                        File.Delete(sourceFile);
                    }
                    catch { }
                }

                if (options.OpenDestFolderOnComplete)
                {
                    string dir = options.SaveInSameFolderAsSourceFile ? Path.GetDirectoryName(sourceFile) : options.DestFolder;
                    if (string.IsNullOrWhiteSpace(dir))
                    {
                        dir = ".\\";
                    }
                    Process process = new Process();
                    process.StartInfo.FileName = dir;
                    process.Start();
                }

                DialogResult = true;
            }
        }

        private FileStream CreateTempStream(out string path)
        {
            string dir = options.SaveInSameFolderAsSourceFile ? Path.GetDirectoryName(sourceFile) : options.DestFolder;
            if (string.IsNullOrWhiteSpace(dir))
            {
                dir = ".\\";
            }
            if (!Directory.Exists(dir))
            {
                Directory.CreateDirectory(dir);
            }
            path = Path.Combine(dir, Path.GetRandomFileName());
            return new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read, FILESTREAM_BUFFER_SIZE, FileOptions.SequentialScan);
        }

        private string RenameTempFile(string tempFile, DateTime start, DateTime end)
        {
            string dir = options.SaveInSameFolderAsSourceFile ? Path.GetDirectoryName(sourceFile) : options.DestFolder;
            if (string.IsNullOrWhiteSpace(dir))
            {
                dir = ".\\";
            }
            if (options.CreateSubFolders)
            {
                dir = Path.Combine(dir, start.ToString("yyyy\\\\M"));
            }

            if (!Directory.Exists(dir))
            {
                Directory.CreateDirectory(dir);
            }

            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("{0:yyyyMMdd_HHmm}_", start);

            TimeSpan duration = end - start;
            if (duration.TotalHours > 1)
            {
                sb.Append(Math.Floor(duration.TotalHours));
                sb.Append('H');
            }
            sb.Append(duration.Minutes);
            sb.Append('M');
            sb.Append(".txt");

            string destFile = Path.Combine(dir, sb.ToString());
            if (File.Exists(destFile))
            {
                File.Delete(destFile);
            }
            File.Move(tempFile, destFile);
            return destFile;
        }
    }
}
